package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.awt.image.Kernel;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;


/**
 * 缓存工具类
 */
@Component
@Slf4j
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 存入任意对象，设置过期时间
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 存入任意对象，设置逻辑删除字段
     *
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        //1.封装RedisData
        RedisData redisData = RedisData.builder()
                .data(value)
                .expireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)))
                .build();
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    /**
     * 解决缓存穿透根据ID查询方法
     */
    public <R, ID> R getWithCacheThrough(String keyProfix, ID id, Class<R> type,
                                         Function<ID, R> dbFallBack,
                                         Long time, TimeUnit unit) {
        String key = keyProfix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //a json转bean
        if (StrUtil.isNotBlank(json)) {
            //若redis中有数据，直接反序列化后返回
            return JSONUtil.toBean(json, type);
        }

        //判断是否为空（“”），不为null为“”，返回空值
        if (json != null) {
            return null;
        }

        //若没有数据则查询数据库
        R r = dbFallBack.apply(id);

        //判断数据库中是否有数据
        if (BeanUtil.isEmpty(r)) {
            //数据库中没有数据时空值存入redis
            log.warn("没有该id的缓存信息：{},向reids存入null", id);
            stringRedisTemplate.opsForValue().set(keyProfix + id, ""
                    , CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //数据库中有数据时存入redis并返回商家数据
        //将shop转为json字符串存入redis,设置有效期(有效期后添加随机值，防止缓存雪崩)
        this.set(key, JSONUtil.toJsonStr(r), time + RandomUtil.randomLong(2L), unit);
        //返回商户信息
        return r;
    }


    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 逻辑过期解决缓存击穿
     *
     * @param id
     * @return
     */
    public  <R, ID> R queryWithLogicalExprie(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit) {
        //1.从redis中获取数据，查看是否命中
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //a json转bean
        if (StrUtil.isBlank(json)) {
            //1.1未命中，返回null
            return null;
        }
        //2.命中
        //3.从缓存中取出redisdata解析出过期时间
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        //4.判断是否过期
        LocalDateTime expire = LocalDateTime.now();
        if (!expire.isAfter(redisData.getExpireTime())) {
            //4.1未过期，返回数据
            return r;
        }
        //4.2已过期，尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean islock = tryLock(lockKey);

        if (islock) {
            //5.获取成功，开启一条新的线程，进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() ->
            {
                try {
                    //重建缓存
                    //从数据库中查询
                    R r1 = dbFallBack.apply(id);

                    //写入redis
                    setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    delLock(lockKey);
                }
            });
        }
        //6.获取失败，返回旧的数据
        return r;
    }

    /**
     * 尝试获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        //1.sexnx
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }


    /**
     * 释放锁
     *
     * @param key
     */
    private void delLock(String key) {
        stringRedisTemplate.delete(key);
    }


}
